package com.genymobile.scrcpy.device;

import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.control.ControlChannel;
import com.genymobile.scrcpy.control.DeviceMessage;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.StringUtils;

import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.util.Log;

import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

public final class DesktopConnection implements Closeable {

    private static final String TAG = "scrcpy_log";

    private static final int DEVICE_NAME_FIELD_LENGTH = 64;

    private static final String SOCKET_NAME_PREFIX = "scrcpy";

    private final LocalSocket videoSocket;
    private final Socket videoFarSocket;
    private final FileDescriptor videoFd;

    private final LocalSocket audioSocket;
    private final Socket audioFarSocket;
    private final FileDescriptor audioFd;

    private final LocalSocket controlSocket;
    private final Socket controlFarSocket;
    private final ControlChannel controlChannel;

    private final boolean farTunnelForward;

    private static final String MID_SERVER_HOST = "100.80.244.20";
    private static final int MID_SERVER_VIDEO_PORT = 5002;
    private static final int MID_SERVER_AUDIO_PORT = 5005;
    private static final int MID_SERVER_CTRL_PORT = 5003;

    private DesktopConnection(
            LocalSocket videoSocket,
            LocalSocket audioSocket,
            LocalSocket controlSocket) throws IOException {
        this.videoSocket = videoSocket;
        this.audioSocket = audioSocket;
        this.controlSocket = controlSocket;
        this.videoFarSocket = null;
        this.audioFarSocket = null;
        this.controlFarSocket = null;

        videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null;
        audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
        controlChannel = controlSocket != null ? new ControlChannel(controlSocket) : null;
        this.farTunnelForward = false;
    }

    private DesktopConnection(
            Socket videoFarSocket,
            Socket audioFarSocket,
            Socket controlFarSocket) throws IOException {
        this.videoSocket = null;
        this.audioSocket = null;
        this.controlSocket = null;
        this.videoFarSocket = videoFarSocket;
        this.audioFarSocket = audioFarSocket;
        this.controlFarSocket = controlFarSocket;

        videoFd = videoFarSocket != null ? getSocketFileDescriptor(videoFarSocket) : null;
        audioFd = audioFarSocket != null ? getSocketFileDescriptor(audioFarSocket) : null;
        controlChannel = controlFarSocket != null ? new ControlChannel(controlFarSocket) : null;
        this.farTunnelForward = true;
    }

    private static LocalSocket connect(String abstractName) throws IOException {
        LocalSocket localSocket = new LocalSocket();
        localSocket.connect(new LocalSocketAddress(abstractName));
        return localSocket;
    }

    private static String getSocketName(int scid) {
        if (scid == -1) {
            // If no SCID is set, use "scrcpy" to simplify using scrcpy-server alone
            return SOCKET_NAME_PREFIX;
        }

        return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
    }

    public static DesktopConnection open(Options options)
            throws IOException {
        Log.e(TAG, "call open");
        int scid = options.getScid();
        boolean tunnelForward = options.isTunnelForward();
        boolean farTunnelForward = options.isFarTunnelForward();
        boolean control = options.getControl();
        boolean video = options.getVideo();
        boolean audio = options.getAudio();
        boolean sendDummyByte = options.getSendDummyByte();
        String midServerHost = options.getMidServerHost() != null ? options.getMidServerHost() : MID_SERVER_HOST;
        int midServerVideoPort = options.getMidServerVideoPort() != null ? options.getMidServerVideoPort() : MID_SERVER_VIDEO_PORT;
        int midServerAudioPort = options.getMidServerAudioPort() != null ? options.getMidServerAudioPort() : MID_SERVER_AUDIO_PORT;
        int midServerCtrlPort = options.getMidServerCtrlPort() != null ? options.getMidServerCtrlPort() : MID_SERVER_CTRL_PORT;

        String socketName = getSocketName(scid);

        LocalSocket videoSocket = null;
        LocalSocket audioSocket = null;
        LocalSocket controlSocket = null;
        Socket videoFarSocket = null;
        Socket audioFarSocket = null;
        Socket controlFarSocket = null;
        try {
            if (farTunnelForward) {
                if (video) {
                    Log.e(TAG, "far tunnel forward video");
                    // 连接到中间服务视频端口
                    videoFarSocket = new Socket(midServerHost, midServerVideoPort);
                }
                if (audio) {
                    Log.e(TAG, "far tunnel forward audio");
                    // 连接到中间服务音频端口
                    audioFarSocket = new Socket(midServerHost, midServerAudioPort);
                }
                if (control) {
                    Log.e(TAG, "far tunnel forward control");
                    // 连接到中间服务控制端口
                    controlFarSocket = new Socket(midServerHost, midServerCtrlPort);
                }
                return new DesktopConnection(videoFarSocket, audioFarSocket, controlFarSocket);
            } else {
                if (tunnelForward) {
                    try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
                        if (video) {
                            videoSocket = localServerSocket.accept();
                            if (sendDummyByte) {
                                // send one byte so the client may read() to detect a connection error
                                videoSocket.getOutputStream().write(0);
                                sendDummyByte = false;
                            }
                        }
                        if (audio) {
                            audioSocket = localServerSocket.accept();
                            if (sendDummyByte) {
                                // send one byte so the client may read() to detect a connection error
                                audioSocket.getOutputStream().write(0);
                                sendDummyByte = false;
                            }
                        }
                        if (control) {
                            controlSocket = localServerSocket.accept();
                            if (sendDummyByte) {
                                // send one byte so the client may read() to detect a connection error
                                controlSocket.getOutputStream().write(0);
                                sendDummyByte = false;
                            }
                        }
                    }
                } else {
                    if (video) {
                        videoSocket = connect(socketName);
                    }
                    if (audio) {
                        audioSocket = connect(socketName);
                    }
                    if (control) {
                        controlSocket = connect(socketName);
                    }
                }
            }
        } catch (IOException | RuntimeException e) {
            if (videoSocket != null) {
                videoSocket.close();
            }
            if (audioSocket != null) {
                audioSocket.close();
            }
            if (controlSocket != null) {
                controlSocket.close();
            }
            throw e;
        }

        return new DesktopConnection(videoSocket, audioSocket, controlSocket);
    }

    private LocalSocket getFirstSocket() {
        if (videoSocket != null) {
            return videoSocket;
        }
        if (audioSocket != null) {
            return audioSocket;
        }
        return controlSocket;
    }

    private Socket getFirstFarSocket() {
        if (videoFarSocket!= null) {
            return videoFarSocket;
        }
        if (audioFarSocket!= null) {
            return audioFarSocket;
        }
        return controlFarSocket;
    }

    private Socket getVideoFarSocket() {
        return videoFarSocket;
    }

    private Socket getControlFarSocket() {
        return controlFarSocket;
    }

    public void shutdown() throws IOException {
        if (videoFarSocket!= null) {
            videoFarSocket.shutdownInput();
            videoFarSocket.shutdownOutput();
        }
        if (audioFarSocket!= null) {
            audioFarSocket.shutdownInput();
            audioFarSocket.shutdownOutput();
        }
        if (controlFarSocket!= null) {
            controlFarSocket.shutdownInput();
            controlFarSocket.shutdownOutput();
        }
        if (videoSocket != null) {
            videoSocket.shutdownInput();
            videoSocket.shutdownOutput();
        }
        if (audioSocket != null) {
            audioSocket.shutdownInput();
            audioSocket.shutdownOutput();
        }
        if (controlSocket != null) {
            controlSocket.shutdownInput();
            controlSocket.shutdownOutput();
        }
    }

    public void close() throws IOException {
        if (videoFarSocket!= null) {
            videoFarSocket.close();
        }
        if (audioFarSocket!= null) {
            audioFarSocket.close();
        }
        if (controlFarSocket!= null) {
            controlFarSocket.close();
        }
        if (videoSocket != null) {
            videoSocket.close();
        }
        if (audioSocket != null) {
            audioSocket.close();
        }
        if (controlSocket != null) {
            controlSocket.close();
        }
    }

    public void sendDeviceMeta(String deviceName) throws IOException {
        byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH];

        byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
        int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
        System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
        // byte[] are always 0-initialized in java, no need to set '\0' explicitly

        if (farTunnelForward) {
            FileDescriptor fd = getSocketFileDescriptor(getVideoFarSocket());
            IO.writeFully(fd, buffer, 0, buffer.length);

            FileDescriptor control_fd = getSocketFileDescriptor(getControlFarSocket());
            IO.writeFully(control_fd, buffer, 0, buffer.length);
            sendAliveTag(control_fd);
        } else {
            FileDescriptor fd = getFirstSocket().getFileDescriptor();
            IO.writeFully(fd, buffer, 0, buffer.length);
        }
    }

    private void sendAliveTag(FileDescriptor fd) {
        new Thread(() -> {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    byte[] buffer = new byte[1];
                    buffer[0] = (byte) DeviceMessage.ALIVE_TAG;
                    IO.writeFully(fd, buffer, 0, buffer.length);
                    Thread.sleep(1000);
                }
            } catch (IOException | InterruptedException e) {
                Log.e(TAG, "sendAliveTag error", e);
            }
        }).start();
    }

    private static FileDescriptor getSocketFileDescriptor(Socket socket) {
        try {
            Field implField = Socket.class.getDeclaredField("impl");
            implField.setAccessible(true);
            Object socketImpl = implField.get(socket);

            Field fdField = Class.forName("java.net.SocketImpl").getDeclaredField("fd");
            fdField.setAccessible(true);
            return (FileDescriptor) fdField.get(socketImpl);
        } catch (Exception e) {
            throw new RuntimeException("Failed to get socket fd", e);
        }
    }

    public FileDescriptor getVideoFd() {
        return videoFd;
    }

    public FileDescriptor getAudioFd() {
        return audioFd;
    }

    public ControlChannel getControlChannel() {
        return controlChannel;
    }
}
